package just.stuff.morsels.data;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import just.stuff.morsels.objects.AssistanceRequest;
import just.stuff.morsels.objects.Label;
import just.stuff.morsels.objects.Morsel;
import just.stuff.morsels.objects.User;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.QueryParser.Operator;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;

import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.SecondaryIndex;
import com.sleepycat.persist.StoreConfig;
import com.sleepycat.persist.evolve.Mutations;
import com.sleepycat.persist.evolve.Renamer;

public class BerkeleyDataSource {

	public static final String CREATE_NEW_ID = "new";
	
	private PrimaryIndex<String, Morsel> morselId;
	private SecondaryIndex< String, String, Morsel> morselByLabel;
	private PrimaryIndex<String, Label> labels;
	private PrimaryIndex<String, User> users;
	private SecondaryIndex< String, String, User> emails;
	private PrimaryIndex<Long, AssistanceRequest> assistance;
	private SecondaryIndex< String, Long, AssistanceRequest> assistanceByMorsel;
	private SecondaryIndex< String, Long, AssistanceRequest> assistanceByUser;
	

	private IndexWriter writer;
	private ExecutorService updater;

	private final SimpleFSDirectory indexDir;
	
	
	public BerkeleyDataSource( File dbRoot, File luceneDir ) throws CorruptIndexException, LockObtainFailedException, IOException {
	    EnvironmentConfig envConfig = new EnvironmentConfig();
	    envConfig.setAllowCreate(true);
	    envConfig.setTransactional( true );
	    envConfig.setTxnTimeout( 5, TimeUnit.SECONDS );
	    Environment env = new Environment(dbRoot, envConfig);
	    
	    StoreConfig sConfig = new StoreConfig();
	    sConfig.setAllowCreate( true );
	    sConfig.setTransactional( true );
	    
	    Mutations m = new Mutations();
	    m.addRenamer( new Renamer( "just.stuff.morsels.ojbects.Morsel", 0, "just.stuff.morsels.objects.Morsel" ));
	    m.addRenamer( new Renamer( "just.stuff.morsels.ojbects.Label", 0, "just.stuff.morsels.objects.Label" ));
	    m.addRenamer( new Renamer( "just.stuff.morsels.ojbects.User", 0, "just.stuff.morsels.objects.User" ));
	    sConfig.setMutations( m );
	    EntityStore store = new EntityStore(env, "MorselStore", sConfig );
	    
	    
	    this.morselId = store.getPrimaryIndex( String.class, Morsel.class );
	    this.morselByLabel = store.getSecondaryIndex( morselId, String.class, "labels" );
	    this.labels = store.getPrimaryIndex( String.class, Label.class );
	    this.users = store.getPrimaryIndex( String.class, User.class );
	    this.emails = store.getSecondaryIndex( users, String.class, "email" );
	    this.assistance = store.getPrimaryIndex( Long.class, AssistanceRequest.class );
	    this.assistanceByMorsel = store.getSecondaryIndex( assistance, String.class, "morselId" );
	    this.assistanceByUser = store.getSecondaryIndex( assistance, String.class, "from" );
	    
			
		IndexWriterConfig config = new IndexWriterConfig( Version.LUCENE_32, new StandardAnalyzer( Version.LUCENE_32 ) );
		//TODO: configure open mode
		config.setOpenMode( OpenMode.CREATE );
		indexDir = new SimpleFSDirectory(  luceneDir );
		this.writer =  new IndexWriter( indexDir,config );
		this.updater = Executors.newCachedThreadPool(); //TODO: configure threadpool
		if( config.getOpenMode() == OpenMode.CREATE )
			indexAll();
	    
	}
	
	public Morsel getRootMorsel() {
		Morsel ret = morselId.get( Morsel.ROOT_ID );
		if( ret == null ) {
			ret = new Morsel();
			ret.setId( Morsel.ROOT_ID );
			ret.setTitle( "Welcome");
			ret.setText( "Welcome to the dummy root.");
			ret.setRelevance( 0 );
			ret.setLastSaved( new Date() );
			morselId.put( ret );
		}
		return ret;
	}

	public Morsel load( String id ) {
		if( CREATE_NEW_ID.equals( id ) ) {
			return new Morsel();
		} else if( Morsel.ROOT_ID.equals( id ) ) {
			return getRootMorsel();
		}
		return morselId.get( id );
	}
	
	public void save(Morsel morsel) {
		if( morsel.getId() == null ) {
			morsel.setId( String.valueOf( morselId.count() + 1 ) );
		} else if( morsel.getId().equals( Morsel.ROOT_ID ) ) {
			morsel.setLabels( null );
		}
		saveLabels( morsel.getLabels() );
		morselId.put( morsel );
		updateIndex( morsel );
	}

	private void saveLabels(Set<String> labels2) {
		if( labels2 != null ) {
			for( String label : labels2 ) {
				if( labels.get( label ) == null ) {
					labels.put( new Label( label ) ) ;
				}
			}
		}
	}

	public List<Morsel> morsels() {
		LinkedList<Morsel> ret = new LinkedList<Morsel>(morselId.sortedMap().values());
		return ret;
		
	}
	
	public List<Morsel> morsels(String label) {
		LinkedList<Morsel> ret = new LinkedList<Morsel>(morselByLabel.subIndex( label ).sortedMap().values());
		Collections.sort( ret );
		return ret;
	}
	
	public List<String> labels() {
		return new LinkedList<String>( labels.sortedMap().keySet() );
	}

	public List<String> labels(String prefix) {
		LinkedList<String> ret = new LinkedList<String>();
		for( String label: labels.keys( prefix, true, null, false ) ) {
			if( label.startsWith( prefix ) ) {
				ret.add( label );
			} else {
				break;
			}
		}
		return ret;
	}

	public void deleteLabel(String label) {
		labels.delete( label );
	}
	

	
	
	private Document buildDocument( Morsel m ) {
		Document ret = new Document();
		ret.add( new Field( "id", m.getId(), Store.YES, Index.NO ) );
		if( m.getLastSaved() != null )
		   ret.add( new Field( "lastSaved", String.valueOf( m.getLastSaved().getTime() ), Store.YES, Index.NO ) );
		if( m.getTitle() != null ) {
		   ret.add( new Field( "title", m.getTitle(), Store.NO, Index.ANALYZED) );
		   ret.add( new Field( "text", m.getTitle(), Store.NO, Index.ANALYZED) );
		}
		if( m.getLabels() != null ) {
			for( String label : m.getLabels() ) {
				ret.add( new Field( "label", label, Store.NO, Index.NOT_ANALYZED ) );
				ret.add( new Field( "text", label, Store.NO, Index.NOT_ANALYZED ) );
			}
		}
		if( m.getText() != null )
		  ret.add( new Field( "text", m.getText(), Store.NO, Index.ANALYZED) );
		
		double score = 2.0d / ( 1.0d + Math.pow( Math.E, -((double)m.getRelevance())/5.0d) );
		
		System.out.println( "Relevance: "+m.getRelevance()+" score: "+score );
		ret.setBoost( (float)score );
		return ret;
	}
	
	public void indexAll() throws CorruptIndexException, LockObtainFailedException, IOException {
		for( Morsel m : morsels() ) {
			writer.addDocument( buildDocument( m ) );
		}
		writer.commit();
		
	}
	
	private void updateIndex( final Morsel m ) {
		updater.execute( new Runnable() {
			public void run() {
				Document doc = buildDocument( m );
				Term t = new Term( "id", m.getId() );
				try {
					writer.updateDocument( t, doc );
					writer.commit();
				} catch (Exception e) {
					//FIXME: exception handling
					throw new RuntimeException( e );
				}
			}
		});
	}

	
	public List<Morsel> findRelated( Morsel m ) throws CorruptIndexException, ParseException, IOException {
		if( m == null || m.getTitle() == null ) {
			return Collections.emptyList();
		}
		QueryParser qp = new QueryParser( Version.LUCENE_32, "text", new StandardAnalyzer( Version.LUCENE_32 ) );
		qp.setDefaultOperator( Operator.OR );
		Query q = qp.parse( m.getTitle() );
		//TODO: share index searcher
		IndexSearcher is = new IndexSearcher(  indexDir, true );
		TopDocs td = is.search( q, 50 );
		LinkedList<Morsel> ret = new LinkedList<Morsel>();
		for( ScoreDoc sd : td.scoreDocs ) {
			Document doc = is.doc( sd.doc );
			String id = doc.get( "id" );
			if( (id != null && id.equals( m.getId() ) ) )
				continue;
			if( id == null )
				continue;
			if( sd.score > 0.2 )
			  ret.add( load( id ) );
		}
		is.close();
		return ret;
	}
	
	public List<Morsel> find( String query ) throws ParseException, CorruptIndexException, IOException {
		QueryParser qp = new QueryParser( Version.LUCENE_32, "text", new StandardAnalyzer( Version.LUCENE_32 ) );
		qp.setDefaultOperator( Operator.AND );
		Query q = qp.parse( query );
		//TODO: share index searcher
		IndexSearcher is = new IndexSearcher(  indexDir, true );
		TopDocs td = is.search( q, 50 );
		LinkedList<Morsel> ret = new LinkedList<Morsel>();
		for( ScoreDoc sd : td.scoreDocs ) {
			Document doc = is.doc( sd.doc );
			String id = doc.get( "id" );
			ret.add( load( id ) );
		}
		is.close();
		return ret;
	}
	
	private SecureRandom random = new SecureRandom();
	
	public User login( String email ) {
		User user = emails.get( email );
		if( user == null ) {
			System.out.println( "Creating new user: "+email );
			user = new User();
			BigInteger id = new BigInteger( 130, random );
			user.setId( id.toString( Character.MAX_RADIX ) );
			user.setEmail( email );
			users.put( user );
		}
		return user;
		
	}

	public User findUserSession( String id ) {
		return users.get( id );
	}
	
	public void save( User user ) {
		if( user.getId() != null ) {
			users.put( user );
		}
	}
	
	public void save( AssistanceRequest req ) {
		assistance.put( req );
	}
	
	public AssistanceRequest getAssistance( Morsel morsel ) {
		return assistanceByMorsel.get( morsel.getId() );
	}

	public void delete(AssistanceRequest req) {
		assistance.delete( req.getId() );
	}

	//FIXME: optimise this
	public List<Morsel> morselsWithAssistance(String label ) {
		List<Morsel> ret = morsels( label );
		Iterator<Morsel> it = ret.iterator();
		while( it.hasNext() ) {
			if( getAssistance( it.next() ) == null ) {
				it.remove();
			}
		}
		return ret;
	}

	public List<Morsel> morselsWithAssistance(User user) {
		List<Morsel> ret = new LinkedList<Morsel>();
		for( AssistanceRequest req : assistanceByUser.subIndex( user.getId() ).sortedMap().values() ) {
			ret.add( load( req.getMorselId() ) );
		}
		return ret;
	}
	
}
